首页 / 技术类 / COM / 在 DLL 中加入第二个 COM 类

在 DLL 中加入第二个 COM 类

2012-09-12 00:23:00

引言

在前面几篇文章里,我们已经成功脱离ATL写了一个COM组件,并且实现了自动化。今天,我们来加入第二个类,并且为加入第二个类做一些整理工作。

@ 为DLL建立一个Module类

在前面,我们为了使得DllCanUnloadNow能正确工作而放了一个全局变量LONG g_nModuleCount,并且在SampleClass的构造函数和析构函数里对它进行自增和自减。另外还有个ITypeLib,也是全局的。为了将这些零散的东西收集在一起,我们建立一个ComModule类,地位类似MFC的CWinApp,作为这个DLL中的唯一的全局对象。

 1class ComModule
 2{
 3public:
 4    ComModule(HMODULE hModule = nullptr) :
 5        m_hModule(hModule), m_nGlobalRefCount(0), m_pTypeLib(nullptr)
 6    {
 7        TCHAR szModulePath[MAX_PATH] = {};
 8        GetModuleFileName(m_hModule, szModulePath, ARRAYSIZE(szModulePath));
 9
10        m_strModulePath = szModulePath;
11
12        LoadTypeLib(szModulePath, &m_pTypeLib);
13    }
14
15    ~ComModule()
16    {
17        if (m_pTypeLib != nullptr)
18        {
19            m_pTypeLib->Release();
20            m_pTypeLib = nullptr;
21        }
22    }
23
24public:
25    ULONG GlobalAddRef()
26    {
27        return (ULONG)InterlockedIncrement(&m_nGlobalRefCount);
28    }
29
30    ULONG GlobalRelease()
31    {
32        return (ULONG)InterlockedDecrement(&m_nGlobalRefCount);
33    }
34
35private:
36    HMODULE   m_hModule;
37    String    m_strModulePath;
38    LONG      m_nGlobalRefCount;
39    ITypeLib *m_pTypeLib;
40};

然后,定义一个全局指针g_pComModule:

1__declspec(selectany) ComModule *g_pComModule = nullptr;

要求在DllMain里面new/delete一个ComModule:

 1BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
 2{
 3    switch (ul_reason_for_call)
 4    {
 5    case DLL_PROCESS_ATTACH:
 6        g_pComModule = new ComModule(hModule);
 7        break;
 8    case DLL_THREAD_ATTACH:
 9        break;
10    case DLL_THREAD_DETACH:
11        break;
12    case DLL_PROCESS_DETACH:
13        delete g_pComModule;
14        break;
15    default:
16        break;
17    }
18
19    return TRUE;
20}

再把全局对象计数放到所有COM类的公共基类ComClass里。

 1template <typename T>
 2class ComClass
 3{
 4public:
 5    ComClass()
 6    {
 7        if (g_pComModule != nullptr)
 8        {
 9            g_pComModule->GlobalAddRef();
10        }
11    }
12
13    ~ComClass()
14    {
15        if (g_pComModule != nullptr)
16        {
17            g_pComModule->GlobalRelease();
18        }
19    }
20
21    // ...
22};

这样,每个COM对象会自动向Module类报告引用计数,这边解决了DllCanUnloadNow的问题。

同时,为了方便建立一个新的DLL,我们把需要导出的那几个函数也都做在ComModule里面,使得DLL那边只需要写下面这些就够了:

 1STDAPI DllCanUnloadNow()
 2{
 3    return xl::g_pComModule->DllCanUnloadNow();
 4}
 5
 6STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv)
 7{
 8    return xl::g_pComModule->DllGetClassObject(rclsid, riid, ppv);
 9}
10
11STDAPI DllRegisterServer()
12{
13    return xl::g_pComModule->DllRegisterServer();
14}
15
16STDAPI DllUnregisterServer()
17{
18    return xl::g_pComModule->DllUnregisterServer();
19}
20
21STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine)
22{
23    return xl::g_pComModule->DllInstall(bInstall, lpszCmdLine);
24}
25
26ComModule::DllCanUnloadNow的实现代码为:
27
28HRESULT DllCanUnloadNow()
29{
30    return m_nGlobalRefCount > 0 ? S_FALSE : S_OK;
31}

COM类的全局映射

现在需要解决的是ComModule::DllGetClassObject。在之前的代码里,我们直接写死一个ClassFactory,ClassFactory里面对应写死的SampleClass。现在要假设有好多COM类的情形。

类厂我们可以搞个模版,ClassFactory,T就是对应的COM类,在类厂里面 new T 就可以了。问题是DllGetClassObject只有CLSID,也就是__uuidof(T),没有T本身。从__uuidof(T)得到T,是件不容易的事情,__uuidof(T)是数据不是类型,没法采用类似萃取的方法。

我们的名言是,不会做了,于就抄ATL的……

不知大家有木有发现,ATL生成的COM类的头文件的最后有一句这样子的话:

1OBJECT_ENTRY_AUTO(__uuidof(CTheClass), TheClass)

(惭愧的是,我曾经有一次把它当成垃圾代码删除掉了,导致对象怎么也创建不出来,还为此调试了好久……)

这个宏将分散在各个头文件的类定义搜集在一起,形成内存连续的全局常量,以便我们在DllGetClassObject之中查表。具体的手法可以参考下面两篇文章: 《巧妙的Section — — 剖析ATL OBJECT_MAP的自动建立》 《The Object Map》

它的手法是挺巧妙,可是太编译器相关了,相关得我不想抄……可是又暂时想不出其他办法,只好抄了……

由于数据里无法存储“类型”,只能存储数据,于是我们为Factory类定义Factory的Factory:

 1template <typename T>
 2class ClassFactory : public ComClass<ClassFactory<T>>,
 3                     public IClassFactoryImpl<>
 4{
 5public:
 6    static IClassFactory *CreateFactory()
 7    {
 8        return new ClassFactory;
 9    }
10
11    // ...
12};

这样,对于每个COM类T,我们只需要存储__uuidof(T)以及ClassFactory::CreateFactory,便可以在DllGetClassObject中查表,由CLSID查到类厂创建函数,从而得到类厂实例。

表结构定义:

1typedef IClassFactory *(*ClassFactoryCreator)();
2
3struct ClassEntry
4{
5    const CLSID        *pClsid;
6    ClassFactoryCreator pfnCreator;
7};

然后抄ATL的手法:

 1#pragma section("XL_COM$__a", read)
 2#pragma section("XL_COM$__m", read)
 3#pragma section("XL_COM$__z", read)
 4
 5extern "C"
 6{
 7    __declspec(selectany) __declspec(allocate("XL_COM$__a"))
 8        const ClassEntry *LP_CLASS_BEGIN = nullptr;
 9    __declspec(selectany) __declspec(allocate("XL_COM$__z"))
10        const ClassEntry *LP_CLASS_END = nullptr;
11}
12
13#if !defined(_M_IA64)
14#pragma comment(linker, "/merge:XL_COM=.rdata")
15#endif
16
17#if defined(_M_IX86)
18#define XL_CLASS_MAP_PRAGMA(class) __pragma(comment(linker, "/include:_LP_CLASS_ENTRY_" # class));
19#elif defined(_M_IA64) || defined(_M_AMD64)
20#define XL_CLASS_MAP_PRAGMA(class) __pragma(comment(linker, "/include:LP_CLASS_ENTRY_" # class));
21#else
22#error Unknown Platform. define XL_CLASS_MAP_PRAGMA
23#endif
24
25#define XL_DECLARE_COM_CLASS(class)                                         \
26                                                                            \
27    const ClassEntry CLASS_ENTRY_##class =                                  \
28    {                                                                       \
29        &__uuidof(class),                                                   \
30        &ClassFactory<class>::CreateFactory                                 \
31    };                                                                      \
32    extern "C" __declspec(allocate("XL_COM$__m")) __declspec(selectany)     \
33        const ClassEntry * LP_CLASS_ENTRY_##class = &CLASS_ENTRY_##class;   \
34    XL_CLASS_MAP_PRAGMA(class)                                              \

呃……虽然上面给出了两篇文章,还是忍不住亲自讲一下。首先是:

1#pragma section("XL_COM$__a", read)
2#pragma section("XL_COM$__m", read)
3#pragma section("XL_COM$__z", read)

看上去是定义了三个段,实际上最后出现在PE文件里的只有一个段。如果没有后面的merge,用工具查看编译出的文件结构,结果是这样的:

新增的自定义的段为“XL_COM”。

实际上编译器在处理段名的时候,只读取到$符号前面的字符,后面的附加后缀只存在于编译期间。目前知道的用途,便是排序,__a会在__m之前,__m会在__z之前。

一开始我们就在 XL_COM$__a和XL_COM$__z分别保存了一个空指针,之后每次调用宏XL_DECLARE_COM_CLASS,就在XL_COM$__m放一个指向真实ClassEntry的指针。

最后的一句,#pragma comment(linker, "/merge:XL_COM=.rdata")用于把段XL_COM合并到.rdata,并保持原来在XL_COM里头的数据不变。

经过以上一系列处理,我们便将一张COM类表保存在了全局数据中。

1__pragma(comment(linker, "/include: ..."))的作用不知道,MSDN解释看不懂,有人知道吗?

最后,ComModule::DllGetClassEntry就可以这样写了:

 1HRESULT DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID *ppv)
 2{
 3    for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
 4    {
 5        if (*ppEntry == nullptr)
 6        {
 7            continue;
 8        }
 9
10        if (rclsid == *(*ppEntry)->pClsid)
11        {
12            IClassFactory *pClassFactory = (*ppEntry)->pfnCreator();
13            return pClassFactory->QueryInterface(riid, ppv);
14        }
15    }
16
17    return CLASS_E_CLASSNOTAVAILABLE;
18}

之所以要判断*ppEntry是否为空,是因为Debug编译的时候,各个指针之间会被插入大量的零数据……ATL也是这么搞的。

这样,我们在每个COM类声明后,也可以像ATL的一样,写一句

1XL_DECLARE_COM_CLASS(SampleClass);

就可以让DllGetClassObject找到对应的类厂了。

只是不知道ATL的OBJECT_ENTRY_AUTO(__uuidof(CTheClass), TheClass)为什么需要我们提供两个参数,第一个参数是CLSID,明显它可以根据第二个参数自己去拿到的嘛……

注册与反注册

注册和反注册部分,我们之前也是写死的,现在要换成活的。

我一开始的方案是,从ComModule里的ITypeLib出发,找到每一个coclass,然后注册CLSID,以及注册TypeLib。但是问题是ProgID,原始IDL文件里面并没有ProgID,因此ITypeLib里面也不可能拿到。我觉得ProgID不能牺牲,所以这个方案不行。

ATL的方案是使用RGS文件,然后注册的时候解析RGS文件并写注册表。我比较讨厌RGS文件……原因是文件格式找不到官方说明,很坑爹的是,字符串值的写法“s ‘SomeString’”的“s”和单引号之间必须有个空格!多少个漆黑的夜里,把rgs改了一下,就再也无法正确注册,然后把原始的拷过来,小心翼翼的一个一个修改、检验,才能发现问题所在……

我决定借用上一节的表,把ClassEntry改为:

1struct ClassEntry
2{
3    const CLSID        *pClsid;
4    ClassFactoryCreator pfnCreator;
5    LPCTSTR             lpszClassDesc;  // SampleClass Class
6    LPCTSTR             lpszProgID;     // COMProvider.SampleClass
7    LPCTSTR             lpszVersion;    // 1
8};

其中的lpszProgID为VersionIndependentProgID,带Version的ProgID使用最后一个lpVersion拼到lpszProgID的后面。

这样,注册Class的函数便可写成类似下面这样子:

 1bool RegisterComClasses(HKEY hRootKey)
 2{
 3    for (const ClassEntry * const *ppEntry = &LP_CLASS_BEGIN + 1; ppEntry < &LP_CLASS_END; ++ppEntry)
 4    {
 5        if (*ppEntry == nullptr)
 6        {
 7            continue;
 8        }
 9
10        // ...
11    }
12
13    return true;
14}

同时可以定义其他三个函数:

代码不贴了。其中TypeLib的信息从ITypeLib中读取。

最后,对外开放的那个两个函数可以简单地实现为:

 1HRESULT DllRegisterServer()
 2{
 3    if (!RegisterTypeLib(HKEY_LOCAL_MACHINE))
 4    {
 5        return E_FAIL;
 6    }
 7
 8    if (!RegisterComClasses(HKEY_LOCAL_MACHINE))
 9    {
10        return E_FAIL;
11    }
12
13    return S_OK;
14}
15
16HRESULT DllUnregisterServer()
17{
18    if (!UnregisterComClasses(HKEY_LOCAL_MACHINE))
19    {
20        return E_FAIL;
21    }
22
23    if (!UnregisterTypeLib(HKEY_LOCAL_MACHINE))
24    {
25        return E_FAIL;
26    }
27
28    return S_OK;
29}

这里只管CLSID、ProgID、TypeLib,AppID啥的不管了。

单用户注册

现在我们来面对之前一直置之不理的DllInstall。函数原型为:

1STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCWSTR pszCmdLine)

MSDN文档页面: http://msdn.microsoft.com/en-us/library/windows/desktop/bb759846.aspx

第一个参数,bInstall,当我们在调用regsvr32的时候指定参数/u的时候,它是FALSE,不指定/u的时候,它是TRUE。

第二个参数,当我们在调用regsvr32指定/i:XXX的时候,pszCmdLine为字符串XXX。当直接使用/i或者/i:的时候,pszCmdLine为空字符串。

只有使用了/i参数,regsvr32才会调用DllInstall。

如果指定了/i,且未指定/n,注册的时候regsvr32会先调用DllRegisterServer,后调用DllInstall(TRUE, …),反注册的时候regsvr32会先调用DllInstall(FALSE, …),后调用DllRegisterServer。 如果指定了/i且指定了/n,regsvr32仅仅调用DllInstall。

好了,文档解释到这里。ATL默认代码为:

 1STDAPI DllInstall(BOOL bInstall, _In_opt_ LPCWSTR pszCmdLine)
 2{
 3    HRESULT hr = E_FAIL;
 4    static const wchar_t szUserSwitch[] = L"user";
 5
 6    if (pszCmdLine != NULL)
 7    {
 8        if (_wcsnicmp(pszCmdLine, szUserSwitch, _countof(szUserSwitch)) == 0)
 9        {
10            ATL::AtlSetPerUserRegistration(true);
11        }
12    }
13
14    if (bInstall)
15    {
16        hr = DllRegisterServer();
17        if (FAILED(hr))
18        {
19            DllUnregisterServer();
20        }
21    }
22    else
23    {
24        hr = DllUnregisterServer();
25    }
26
27    return hr;
28}

其中的“ATL::AtlSetPerUserRegistration(true)”据说会把DllRegisterServer和DllUnregisterServer的注册表位置由HKLM重定向到HKCU。也就是说,当使用regsvr32 /i:user的时候,支持注册到当前用户。

既然ATL默认生成的COM DLL支持/i:user参数,我们也要模拟一下,装得正式一点:

 1HRESULT DllInstall(BOOL bInstall, _In_opt_ LPCTSTR lpszCmdLine)
 2{
 3    if (lpszCmdLine == nullptr)
 4    {
 5        return E_INVALIDARG;
 6    }
 7
 8    if (_tcsicmp(lpszCmdLine, _T("User")) == 0)
 9    {
10        if (bInstall)
11        {
12            if (!RegisterTypeLib(HKEY_CURRENT_USER))
13            {
14                return E_FAIL;
15            }
16
17            if (!RegisterComClasses(HKEY_CURRENT_USER))
18            {
19                return E_FAIL;
20            }
21
22            return S_OK;
23        }
24        else
25        {
26            if (!UnregisterComClasses(HKEY_CURRENT_USER))
27            {
28                return E_FAIL;
29            }
30
31            if (!UnregisterTypeLib(HKEY_CURRENT_USER))
32            {
33                return E_FAIL;
34            }
35
36            return S_OK;
37        }
38    }
39
40    return E_FAIL;
41}

好了,到目前为止我们已经支持了所有标准的注册方式了。除了/i:user,其实我们可以在这里做一些扩展,支持一些自定义的注册方式,来脱离注册表依赖,此为后话。

第二个COM类

准备工作做完了,现在着手加入第二个COM类。

且慢,还有一点点荣誉。在上一篇,为了实现自动化,我们在对象类加入了IDispatch的实现代码。但这些代码是机械的、可抄的,因此可以写成一个独立的东西,IDispatchImpl已经被占用了,就叫Dispatcher吧。

现在SampleClass干净了,代码清单为: SampleClass.h

 1#include "COMProvider_h.h"
 2#include <xl/Win32/COM/xlDispatcher.h>
 3
 4class SampleClass : public xl::ComClass<SampleClass>,
 5                    public xl::Dispatcher<ISampleInterface>
 6{
 7public:
 8    STDMETHOD(SampleMethod)();
 9
10public:
11    XL_COM_INTERFACE_BEGIN(SampleClass)
12        XL_COM_INTERFACE(ISampleInterface)
13        XL_COM_INTERFACE(IDispatch)
14    XL_COM_INTERFACE_END()
15};
16
17XL_DECLARE_COM_CLASS(SampleClass,
18                     _T("Streamlet COMProvider Sample Class"),
19                     _T("Streamlet.COMProvider.SampleClass"),
20                     _T("1"));
21
22SampleClass.cpp
23#include "SampleClass.h"
24
25STDMETHODIMP SampleClass::SampleMethod()
26{
27    MessageBox(NULL, _T("SampleMethod called."), _T("Info"), MB_OK | MB_ICONINFORMATION);
28    return S_OK;
29}

挺清晰的吧?

第二个COM类的IDL:

import "oaidl.idl";
import "ocidl.idl";


[
    object,
    uuid(83C783E3-F989-4E0D-BFC5-631273EDFFDA),
]
interface ISampleInterface : IDispatch
{
    [id(1)] HRESULT SampleMethod();
};
[
    object,
    uuid(AD6AD24D-0E31-44A6-A2B3-7B180437541D),
]
interface ISampleInterface2 : IDispatch
{
    [id(1)] HRESULT SampleMethod2();
};

[
    uuid(22935FC2-282E-4727-B40F-E55128EA1072),
    version(1.0),
]
library COMProviderLib
{
importlib("stdole2.tlb");
     [
        uuid(0DECBFF5-A8A5-49E8-9962-3D18AAC6088E)     
    ]
    coclass SampleClass
    {
         [default] interface ISampleInterface;
    };
    [
        uuid(85431E6A-28C1-483D-A3DE-CEA640899E0E)    
    ]
    coclass SampleClass2
    {
        [default] interface ISampleInterface2;
    };
};

import "shobjidl.idl";

注意,我这里加“2”并不表示同一ProgID的升级版,我只是为了和第一个区分。

SampleClass2的实现代码: SampleClass2.h

 1#include "COMProvider_h.h"
 2#include <xl/Win32/COM/xlDispatcher.h>
 3
 4class SampleClass2 : public xl::ComClass<SampleClass2>,
 5                     public xl::Dispatcher<ISampleInterface2>
 6{
 7public:
 8    STDMETHOD(SampleMethod2)();
 9
10public:
11    XL_COM_INTERFACE_BEGIN(SampleClass2)
12        XL_COM_INTERFACE(ISampleInterface2)
13        XL_COM_INTERFACE(IDispatch)
14    XL_COM_INTERFACE_END()
15};
16
17XL_DECLARE_COM_CLASS(SampleClass2,
18                     _T("Streamlet COMProvider Sample Class 2"),
19                     _T("Streamlet.COMProvider.SampleClass2"),
20                     _T("1"));

SampleClass2.cpp

1STDMETHODIMP SampleClass2::SampleMethod2()
2{
3    MessageBox(NULL, _T("SampleMethod2 called."), _T("Info"), MB_OK | MB_ICONINFORMATION);
4    return S_OK;
5}

完毕。相关框架代码见: http://xllib.codeplex.com/SourceControl/changeset/view/19794#318174 例子见COMProtocol3.rarhttp://pan.baidu.com/s/1hqtJX6c)。

我们走到哪里了? 最近发的比较勤,究竟做了些什么事呢?小小的理一下:

首先,在第一篇《裸写一个含内嵌IE控件的窗口》中,练习了一下如何手工写一个COM类,此时整个模块还不是COM组件。
第二篇《学习下 ATL 的继承链处理》则是个小铺垫,揭示了COM类继承处理上的一个小手法。
第三篇《山寨一下ATL的COM_INTERFACE》是比较全面地学习ATL关于单个COM类的实现上的简化技巧。
第四篇《写个含 Windows Media Player 的窗口》是个整理,验证下前一篇实践代码的合理性和可用性。
第五篇《裸写一个进程内 COM 组件》,是对COM DLL的一个整体实践,不再是单个COM类了。
第六篇《让COM组件可被跨语言调用》是实现自动化,在后面它将作为COM组件的基本功能存在。
第七篇,也就是本文,是对COM DLL的一个整理,把共性整合到框架,简化COM类中的代码,让加入第二个、第三个COM类更方便。

经过这几天的练习,攒下了一批框架性代码,借助于它们,已经可以较为方便地写出一个COM DLL了,就如……使用ATL一样方便。换句话说,我们已经实现了ATL……的一小部分。这是造轮子吗?非也!这是学习过程中的自然积累。

前面的有些做法是抄ATL的,这并不可耻,好方法就拿来用嘛。目前的讨论还仅局限于进程内组件,进程外的并未涉及,COM的加载过程也没有涉及。这些以后再慢慢学。到本文为之,算一个小系列吧,故此总结。


首发:http://www.cppblog.com/Streamlet/archive/2012/09/12/190331.html



NoteIsSite/0.4